/* !
 * elasticlunr.Pipeline
 * Copyright (C) @YEAR Oliver Nightingale
 * Copyright (C) @YEAR Wei Song
 */

/**
   * elasticlunr.Pipelines maintain an ordered list of functions to be applied to
   * both documents tokens and query tokens.
   *
   * An instance of elasticlunr.Index will contain a pipeline
   * with a trimmer, a stop word filter, an English stemmer. Extra
   * functions can be added before or after either of these functions or these
   * default functions can be removed.
   *
   * When run the pipeline, it will call each function in turn.
   *
   * The output of the functions in the pipeline will be passed to the next function
   * in the pipeline. To exclude a token from entering the index the function
   * should return undefined, the rest of the pipeline will not be called with
   * this token.
   *
   * For serialisation of pipelines to work, all functions used in an instance of
   * a pipeline should be registered with elasticlunr.Pipeline. Registered functions can
   * then be loaded. If trying to load a serialised pipeline that uses functions
   * that are not registered an error will be thrown.
   *
   * If not planning on serialising the pipeline then registering pipeline functions
   * is not necessary.
   *
   * @constructor
   */

export default function (elasticlunr) {

  elasticlunr.Pipeline = function () {
    this._queue = [];
  };

  elasticlunr.Pipeline.registeredFunctions = {};

  /**
   * Register a function in the pipeline.
   *
   * Functions that are used in the pipeline should be registered if the pipeline
   * needs to be serialised, or a serialised pipeline needs to be loaded.
   *
   * Registering a function does not add it to a pipeline, functions must still be
   * added to instances of the pipeline for them to be used when running a pipeline.
   *
   * @param {Function} fn The function to register.
   * @param {String} label The label to register this function with
   * @memberOf Pipeline
   */

  elasticlunr.Pipeline.registerFunction = function (fn, label) {
    if (label in elasticlunr.Pipeline.registeredFunctions) {
      elasticlunr.utils.warn('Overwriting existing registered function: ' + label);
    }

    fn.label = label;
    elasticlunr.Pipeline.registeredFunctions[label] = fn;
  };

  /**
   * Get a registered function in the pipeline.
   *
   * @param {String} label The label of registered function.
   * @return {Function}
   * @memberOf Pipeline
   */
  elasticlunr.Pipeline.getRegisteredFunction = function (label) {
    if ((label in elasticlunr.Pipeline.registeredFunctions) !== true) {
      return null;
    }

    return elasticlunr.Pipeline.registeredFunctions[label];
  };

  /**
   * Warns if the function is not registered as a Pipeline function.
   *
   * @param {Function} fn The function to check for.
   * @private
   * @memberOf Pipeline
   */
  elasticlunr.Pipeline.warnIfFunctionNotRegistered = function (fn) {
    var isRegistered = fn.label && (fn.label in this.registeredFunctions);

    if (!isRegistered) {
      elasticlunr.utils.warn(
        'Function is not registered with pipeline. ' +
        'This may cause problems when serialising the index.\n',
        fn
      );
    }
  };

  /**
   * Loads a previously serialised pipeline.
   *
   * All functions to be loaded must already be registered with elasticlunr.Pipeline.
   * If any function from the serialised data has not been registered then an
   * error will be thrown.
   *
   * @param {Object} serialised The serialised pipeline to load.
   * @return {elasticlunr.Pipeline}
   * @memberOf Pipeline
   */
  elasticlunr.Pipeline.load = function (serialised) {
    var pipeline = new elasticlunr.Pipeline();

    serialised.forEach(function (fnName) {
      var fn = elasticlunr.Pipeline.getRegisteredFunction(fnName);

      if (fn) {
        pipeline.add(fn);
      } else {
        throw new Error('Cannot load un-registered function: ' + fnName);
      }
    });

    return pipeline;
  };

  /**
   * Adds new functions to the end of the pipeline.
   *
   * Logs a warning if the function has not been registered.
   *
   * @param {Function} functions Any number of functions to add to the pipeline.
   * @memberOf Pipeline
   */
  elasticlunr.Pipeline.prototype.add = function () {
    var fns = Array.prototype.slice.call(arguments);

    fns.forEach(function (fn) {
      elasticlunr.Pipeline.warnIfFunctionNotRegistered(fn);
      this._queue.push(fn);
    }, this);
  };

  /**
   * Adds a single function after a function that already exists in the
   * pipeline.
   *
   * Logs a warning if the function has not been registered.
   * If existingFn is not found, throw an Exception.
   *
   * @param {Function} existingFn A function that already exists in the pipeline.
   * @param {Function} newFn The new function to add to the pipeline.
   * @memberOf Pipeline
   */
  elasticlunr.Pipeline.prototype.after = function (existingFn, newFn) {
    var pos;

    elasticlunr.Pipeline.warnIfFunctionNotRegistered(newFn);
    pos = this._queue.indexOf(existingFn);

    if (pos === -1) {
      throw new Error('Cannot find existingFn');
    }

    this._queue.splice(pos + 1, 0, newFn);
  };

  /**
   * Adds a single function before a function that already exists in the
   * pipeline.
   *
   * Logs a warning if the function has not been registered.
   * If existingFn is not found, throw an Exception.
   *
   * @param {Function} existingFn A function that already exists in the pipeline.
   * @param {Function} newFn The new function to add to the pipeline.
   * @memberOf Pipeline
   */
  elasticlunr.Pipeline.prototype.before = function (existingFn, newFn) {
    var pos;

    elasticlunr.Pipeline.warnIfFunctionNotRegistered(newFn);
    pos = this._queue.indexOf(existingFn);

    if (pos === -1) {
      throw new Error('Cannot find existingFn');
    }

    this._queue.splice(pos, 0, newFn);
  };

  /**
   * Removes a function from the pipeline.
   *
   * @param {Function} fn The function to remove from the pipeline.
   * @memberOf Pipeline
   */
  elasticlunr.Pipeline.prototype.remove = function (fn) {
    var pos = this._queue.indexOf(fn);

    if (pos === -1) {
      return;
    }

    this._queue.splice(pos, 1);
  };

  /**
   * Runs the current list of functions that registered in the pipeline against the
   * input tokens.
   *
   * @param {Array} tokens The tokens to run through the pipeline.
   * @return {Array}
   * @memberOf Pipeline
   */
  elasticlunr.Pipeline.prototype.run = function (tokens) {
    if (!this._queue.length) {
      return tokens;
    }
    return this._queue.reduce(function (out, queueEntry) {
      return out.reduce(function (state, element, i) {
        var output = queueEntry(element, i, out);

        if (!Array.isArray(output)) output = [output];
        output.filter(function (e) {
          return e !== null && e !== void 0;
        }).forEach(function (item) {
          state.push(item);
        });
        return state;
      }, []).filter(function (e) {
        return e !== '';
      });
    }, tokens);
  };

  /**
   * Resets the pipeline by removing any existing processors.
   *
   * @memberOf Pipeline
   */
  elasticlunr.Pipeline.prototype.reset = function () {
    this._queue = [];
  };

  /**
    * Get the pipeline if user want to check the pipeline.
    *
    * @memberOf Pipeline
    */
  elasticlunr.Pipeline.prototype.get = function () {
    return this._queue;
  };

  /**
   * Returns a representation of the pipeline ready for serialisation.
   * Only serialize pipeline function's name. Not storing function, so when
   * loading the archived JSON index file, corresponding pipeline function is
   * added by registered function of elasticlunr.Pipeline.registeredFunctions
   *
   * Logs a warning if the function has not been registered.
   *
   * @return {Array}
   * @memberOf Pipeline
   */
  elasticlunr.Pipeline.prototype.toJSON = function () {
    return this._queue.map(function (fn) {
      elasticlunr.Pipeline.warnIfFunctionNotRegistered(fn);
      return fn.label;
    });
  };
};
